<!--
 * @Description: 虚拟列表
 * @Author: liyujie
 * @Date: 2022/2/23
 * @Last Modified by: liyujie
 * @Last Modified time: 2022/2/23
-->
<template>
    <section class="wrapper">
        <!--滚动容器-->
        <div
            class="scroll-wrapper"
            ref="scrollWrapper"
            @scroll="handleScroll"
        >
            <!--占位用于形成滚动条-->
            <div
                class="blank"
                ref="blank"
            />
            <!--列表-->
            <div
                class="list"
                ref="list"
                :style="{transform: getTransform}"
            >
                <div
                    class="item"
                    ref="items"
                    v-for="(item,index) in visibleData"
                    :key="index"
                >
                    {{ item.message }}
                </div>
            </div>
        </div>
    </section>
</template>

<script>
import faker from "faker";

export default {
    name: 'VirtualList',
    data() {
        return {
            listData: [],
            // 可视区域高度
            screenHeight: 0,
            // 起始索引
            startIndex: 0,
            // 结束索引
            endIndex: null,
            // 预估高度
            estimatedItemSize: 100,
            positions: []
        };
    },
    computed: {
        // 列表的总高度
        listHeight() {
            return this.positions[this.positions.length - 1].bottom;
        },

        // 一屏可显示的列表项数
        visibleCount() {
            return Math.ceil(this.screenHeight / this.estimatedItemSize);
        },

        // 真实显示的数据
        visibleData() {
            let startIndex = this.startIndex - this.aboveCount;
            let endIndex = this.endIndex + this.belowCount;

            return this.listData.slice(startIndex, endIndex);
        },

        // 列表纵向偏移
        getTransform(){
            return `translateY(${this.startOffset}px)`;
        },

        aboveCount(){
            return Math.min(this.startIndex, this.visibleCount);
        },

        belowCount(){
            return Math.min(this.listData.length - this.endIndex, this.visibleCount);
        }
    },
    mounted() {
        for (let i = 0; i < 10000; i++) {
            this.listData.push({ message: faker.lorem.sentences() });
        }
        // // 获取滚动容器高度
        this.screenHeight = this.$refs.scrollWrapper.clientHeight;
        this.startIndex = 0;
        // // 结束索引为 开始索引 + 一屏显示个数
        this.endIndex = this.startIndex + this.visibleCount;

        this.initPositions();
    },

    updated(){
        this.$nextTick(() => {
            if (!this.$refs.items || !this.$refs.items.length) {
                return;
            }
            //获取真实元素大小，修改对应的尺寸缓存
            this.updateItemsSize();
            //更新列表总高度
            let height = this.positions[this.positions.length - 1].bottom;

            this.$refs.blank.style.height = height + "px";
            //更新真实偏移量
            this.setStartOffset();
        });
    },

    methods: {

        /**
         * 初始化位置信息
         */
        initPositions(){
            this.positions = this.listData.map((item,index)=>{
                return {
                    index,
                    height: this.estimatedItemSize,
                    top: index * this.estimatedItemSize,
                    bottom: (index + 1) * this.estimatedItemSize
                };
            });
        },

        /**
         * 获取列表项的当前尺寸
         */
        updateItemsSize() {
            let nodes = this.$refs.items;

            nodes.forEach(node => {
                let rect = node.getBoundingClientRect();
                let height = rect.height;
                let index = +node.id.slice(1);
                let oldHeight = this.positions[index].height;
                let dValue = oldHeight - height;

                //存在差值
                if (dValue) {
                    this.positions[index].bottom = this.positions[index].bottom - dValue;
                    this.positions[index].height = height;

                    for (let k = index + 1; k < this.positions.length; k++) {
                        this.positions[k].top = this.positions[k - 1].bottom;
                        this.positions[k].bottom = this.positions[k].bottom - dValue;
                    }
                }
            });
        },

        /**
         * 获取当前的偏移量
         */
        setStartOffset() {
            let startOffset =
                this.startIndex >= 1 ? this.positions[this.startIndex - 1].bottom : 0;

            this.$refs.list.style.transform = `translateY(${startOffset}px)`;
        },

        /**
         * 滚动事件
         * @param $event
         */
        handleScroll($event) {
            let scrollTop = this.$refs.scrollWrapper.scrollTop;

            this.startIndex = this.getStartIndex(scrollTop);
            this.endIndex = this.startIndex + this.visibleCount;
            // 此时的偏移量
            this.setStartOffset();
        },

        /**
         * 获取列表起始索引
         * @param scrollTop
         * @returns {*}
         */
        getStartIndex(scrollTop = 0) {
            // 二分法查找
            return this.binarySearch(this.positions, scrollTop);
        },

        /**
         * 二分法查找
         * @param list
         * @param value
         * @returns {null|number}
         */
        binarySearch(list, value) {
            let start = 0;
            let end = list.length - 1;
            let tempIndex = null;

            while (start <= end) {
                let midIndex = parseInt((start + end) / 2);
                let midValue = list[midIndex].bottom;

                if (midValue === value) {
                    return midIndex + 1;
                } else if (midValue < value) {
                    start = midIndex + 1;
                } else if (midValue > value) {
                    if (tempIndex === null || tempIndex > midIndex) {
                        tempIndex = midIndex;
                    }
                    end = end - 1;
                }
            }
            return tempIndex;
        },
    }
};
</script>

<style scoped lang="scss">
.wrapper {
    overflow: hidden;
}
.scroll-wrapper {
    margin: 50px 200px;
    width: 400px;
    height: 800px;
    border: 1px solid #ccc;
    overflow: auto;
    position: relative;
    .blank {
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        z-index: -1;
    }
    .list {
        .item {
            padding: 12px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-bottom: 1px solid #ccc;
            background: #42b983;
        }
    }
}
</style>
